package cn.edu.seu.java.node;

import java.lang.reflect.Modifier;
import java.util.LinkedList;
import java.util.List;
import java.util.Vector;

import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.AnnotationTypeDeclaration;
import org.eclipse.jdt.core.dom.AnonymousClassDeclaration;
import org.eclipse.jdt.core.dom.BlockComment;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.EnumDeclaration;
import org.eclipse.jdt.core.dom.IExtendedModifier;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.Javadoc;
import org.eclipse.jdt.core.dom.LineComment;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.TagElement;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.TypeDeclarationStatement;

import cn.edu.seu.java.ast.visitor.McCabeMethodVisitor;

import cn.edu.seu.java.util.ASTNodeUtil;
import cn.edu.seu.java.util.StringUtil;

public class ASTClass extends JavaNode {
	private TypeDeclaration typeDeclaration;
	private String fullyQualifiedName;
	private String fullSuperClassName;
	private Vector<String> fullSuperInterfaceNames;
	private int wmc;
	private boolean wmcHasBeenCalculated;
	private String relativeSourcePath;
	private String fullyQualifiedTopClassName;
	private boolean tccHasBeenCalculated;
	private double TCC;

	public ASTClass(String sourceFilePath, TypeDeclaration node) {
		this.sourceFilePath = sourceFilePath;
		this.typeDeclaration = node;

		this.tccHasBeenCalculated = false;
		this.TCC = 0.0;

		this.fullyQualifiedName = ASTNodeUtil.getFullyQualifiedName(node);
		this.fullyQualifiedTopClassName = ASTNodeUtil.getFullyQualifiedTopClassName(node);

		this.fullSuperInterfaceNames = new Vector<String>();
		this.fullSuperClassName = "";
		Type type = this.typeDeclaration.getSuperclassType();
		if (type != null) {
			ITypeBinding iTypeBinding = type.resolveBinding();
			if (iTypeBinding != null) {
				String qualifiedSuperclassName = iTypeBinding.getBinaryName(); // may be null
				if (qualifiedSuperclassName != null) {
					this.fullSuperClassName = qualifiedSuperclassName.replaceAll("\\$[0-9]+", "").replaceAll("\\$", ".");
				}
			}
		}

		@SuppressWarnings("unchecked")
		List<Type> interfaces = node.superInterfaceTypes();
		for (Type t : interfaces) {
			ITypeBinding iTypeBinding = t.resolveBinding();
			if (iTypeBinding != null) {
				String binaryName = iTypeBinding.getBinaryName(); // may be null
				if (binaryName != null) {
					this.fullSuperInterfaceNames.add(StringUtil.convert(binaryName));
				}
			}
		}
	}

	public boolean isAbstract() {
		int modifer = this.typeDeclaration.getModifiers();
		return Modifier.isAbstract(modifer);
	}

	public boolean isInterface() {
		return this.typeDeclaration.isInterface();
	}

	public boolean isTopLevelClass() {
		ITypeBinding iTypeBinding = this.typeDeclaration.resolveBinding();
		if (iTypeBinding != null) {
			return iTypeBinding.isTopLevel();
		}

		// try again if fail to binding
		boolean result = true;
		ASTNode parentNode = this.typeDeclaration.getParent();
		while (parentNode.getNodeType() != ASTNode.COMPILATION_UNIT) {
			if (parentNode.getNodeType() == ASTNode.TYPE_DECLARATION) {
				// bug fix: add a condition to teminate while loop 
				result = false;
				break;
			}
			parentNode = parentNode.getParent();
		}
		return result;
	}

	public boolean hasSuperclass() {
		// only consider super class (e.g., like "XX extends YY")
		Type type = typeDeclaration.getSuperclassType();
		return type != null;
	}
	
	/**
	 * 
	 * @return full superclass name if exist, otherwise return empty string.
	 */
	public String getFullSuperclassName() {
		return this.fullSuperClassName;
	}

	public Vector<String> getFullSuperInterfaceNames() {
		return fullSuperInterfaceNames;
	}

	public TypeDeclaration getTypeDeclaration() {
		return typeDeclaration;
	}

	/**
	 * WMC: weighted method in class
	 * @return WMC metric for a given class
	 */
	public int getWMC() {
		if (this.wmcHasBeenCalculated == true) {
			return this.wmc;
		} else {
			return this.sumEachMethodCyclomaticComplexity();
		}
	}

	private int sumEachMethodCyclomaticComplexity() {
		List<MethodDeclaration> allMethods = new LinkedList<MethodDeclaration>();

		// collecting all methods in the measured class
		typeDeclaration.accept(new ASTVisitor() {
			@Override
			public boolean visit(AnonymousClassDeclaration node) {
				// do not consider the method call within anonymous class
				return false;
			}

			@Override
			public boolean visit(TypeDeclarationStatement node) {
				// do not consider the method call within local class (localizing in method's body)
				return false;
			}

			@Override
			public boolean visit(MethodDeclaration node) {
				allMethods.add(node);
				return true;
			}
		});

		// sum all cyclomatic complexities of each method
		for (MethodDeclaration method : allMethods) {
			if (method.getBody() != null) {
				McCabeMethodVisitor visitor = new McCabeMethodVisitor(method);
				wmc = wmc + visitor.getMethodCyclomaticComplexity();
			}
		}

		// important, remember whether CC value has been calcluated
		this.wmcHasBeenCalculated = true;

		return this.wmc;
	}

	@Override
	public int hashCode() {
		HashCodeBuilder b = new HashCodeBuilder(17, 37);
		return b.append(this.fullyQualifiedName).toHashCode();
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj == null) {
			return false;
		}
		if (getClass() != obj.getClass()) {
			return false;
		}
		EqualsBuilder eb = new EqualsBuilder();
		ASTClass other = (ASTClass) obj;
		return eb.append(this.fullyQualifiedName, other.getFullyQualifiedName()).isEquals();
	}

	@Override
	public String toString() {
		return this.sourceFilePath + "\n" + this.fullyQualifiedName;
	}

	public void setRelativeSourcePath(String relativeSourcePath) {
		this.relativeSourcePath = relativeSourcePath;
	}

	public String getRelativeSourceFilePath() {
		return relativeSourcePath;
	}

	/**
	 * 
	 * @return packageName + chainedClassName
	 */
	public String getFullyQualifiedName() {
		return this.fullyQualifiedName;
	}

	public String getFullyQualifiedTopClassName() {
		return fullyQualifiedTopClassName;
	}
}
